#coding=utf-8
# repository.py - utilitis for working with repository
#
# Copyright 2011 Andrey Galibin <andrey.galibin@gmail.com>
# Copyright 2011-2013 Maxim Petrov <maximpetrov@yahoo.com>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 3 or any later version.

import mercurial
from mercurial import ui, hg, match, context, cmdutil, discovery, bundlerepo, exchange
from mercurial import commands, util, filemerge, mdiff
from mercurial.lock import release
from prompt_window import PromptWindow, PasswordPromptWindow
from ok_cancel_window import OKCancelWindow
from message_window import ErrorWindow
import shutil
import os
import sys
SERVER_ERROR_MESSAGE = _(u'Fetch data from remote server failed')
NO_SERVER_ERROR_MESSAGE = _(u'No remote repository specified')

class WrongRepositoryPathError(Exception):
    """Exception raised when there is no repository found in path"""

    def __init__(self, path, main_win):
        self.__path = path
        self.__main_win = main_win

    def __str__(self):
        return _(u'No repository found in \'{0}\'').format(self.__main_win.str_to_internal(self.__path))

    def __unicode__(self):
        return _(u'No repository found in \'{0}\'').format(self.__main_win.str_to_internal(self.__path))

class MyUi(ui.ui):
    """Class - child of mercurial.ui.ui. Redefines prompt as window"""

    def __init__(self, src=None, main_win=None):

        super(MyUi, self).__init__(src)
        if src is None:
            self.__main_win = main_win
        else:
            self.__main_win = src.__main_win
            self.quiet = src.quiet

    def prompt(self, msg, default='y'):
        """Replace for ui.prompt"""

        prompt_win = PromptWindow(self.__main_win.str_to_internal(msg))
        res = self.__main_win.show_modal_window(prompt_win)
        return self.__main_win.str_to_external(res.get(u'value')) or default

    def getpass(self, prompt = _('password: '), default=None):
        """Replace for ui.getpass"""

        pass_win = PasswordPromptWindow(self.__main_win.str_to_internal(prompt))
        res = self.__main_win.show_modal_window(pass_win)
        return self.__main_win.str_to_external(res.get(u'value')) or default

class MessageBuffer(object):
    def __init__(self, main_win):
        self.__buf = u''
        self.__main_win = main_win
    def __get_new_lines(self):
        return '\n'
    """Символ перевода строки"""
    newlines = property(__get_new_lines)

    """
    Атрибут для управления пробелами оператора print
    Подробнее: http://docs.python.org/library/stdtypes.html#bltin-file-objects
    """
    softspace = 0

    def get_content(self):
        s = self.__buf
        self.__buf = u''
        return s

    def close(self):
        """
        Закрытие потока (не реализовано)
        """
        raise IOError("Not implemented")

    def flush(self):
        """
        Опустошает буфер выходного потока.
        """
        pass

    def fileno(self):
        """
        Вовзращает целочисленный дескриптор файла. не реализовано
        """
        raise IOError("Not implemented")

    def isatty(self):
        """
        Является ли объект терминалом.
        На самом деле не является, но поскольку mercurial проверяет это
        даже там, где в этом нет необходимости, то "обманывает" и вовзращает True
        """
        return True

    def next(self):
        """
        Метод для итерации по строкам. Не реализован, так как в mercurial не
        вызывается
        """
        raise IOError("Not implemented")

    def read(self, size=None):
        """
        Не реализовано
        """
        raise IOError("Not implemented")


    def readline(self, size=None):
        """
        Не реализовано
        """
        raise IOError("Not implemented")

    def seek(self, offset, whence=0):
        """
        Перемещается на заданную позицию в файловом потоке (не реализовано)
        Подробнее: http://docs.python.org/library/stdtypes.html#bltin-file-objects
        Аргументы:
        offset -
        whence -
        """
        raise IOError("Not implemented")

    def tell(self):
        """
        Возращает позицию в файловом потоке (не реализовано)
        Подробнее: http://docs.python.org/library/stdtypes.html#bltin-file-objects
        """
        raise IOError("Not implemented")

    def truncate(self, size=None):
        """
        Уменьшает размер файла (путём отрезания "хвоста") (не реализовано)
        Подробнее: http://docs.python.org/library/stdtypes.html#bltin-file-objects

        Аргументы:
            size - число байт
        """
        raise IOError("Not implemented")

    def write(self, str, hist=True):
        """
        Записывает строку str в выходной поток.

        Аргументы:
            str - записываемая строка
            hist - записывать ли строку в историю консоли (по умолчанию - True)
        """
        self.__buf = u''.join([self.__buf, self.__main_win.str_to_internal(str)])

    def writelines(self, sequence):
        """
        Записывает последовательность строк в файловый поток

        Аргументы:
            sequence - последовательность строк
        """
        for s in sequence:
            self.write(s)

def repo_or_peer(repo):
    """
    Function returns peer instead of repository if needed (if Mercurial
    version is 2.3 or later)

    Keyword argumets:
        repo -- input repo (in most cases this is bundle repo)

    Return value:
        input repo or it's peer depends to Mercurial version
    """

    if util.version() >= '2.3':
        if repo is not None:
            return repo.peer()
        else:
            return None
    else:
        return repo

class Repository(object):
    """

    """
    user = None
    repo = None
    server_error = None
    local_error = None
    __bundle_repo = None
    __supposed_bundle_path = None
    __success_bundle_path = None
    __updating = False

    # dict of all remote repos connected in current session
    # this repos already connected and doesn't require authorization
    __remote_repos = {}

    def get_remote_repo(self, path):
        """
        Getter for remote repo. Connect to remote repo if it still not connected.

        Keyword arguments:
            path -- path to remote repo

        Return value:
            repository object for specified path
        """

        if self.__remote_repos.has_key(path):
            remote_repo = self.__remote_repos[path]
        else:
            remote_repo = hg.peer(self.repo.ui, opts={}, path=path)
            self.__remote_repos[path] = remote_repo
        return remote_repo

    @property
    def bundle_repo(self):
        """Getter for __bundle_repo"""

        return self.__bundle_repo

    @bundle_repo.setter
    def bundle_repo(self, repo):
        """Setter for __bundle_repo"""

        self.__bundle_repo = repo

    bundle_diff = None
    clear_bundle = None
    commoninc = None


    def __init__(self, workdir, main_win, user=None, console=None):

        self.main_win = main_win
        self.workdir = main_win.str_to_external(workdir)
        self.path = cmdutil.findrepo(self.workdir)
        if self.path is None:
            raise WrongRepositoryPathError(workdir, main_win)
        con_ui = MyUi(main_win=main_win)
        if console is not None:
            con_ui.fout = console
            con_ui.ferr = console

        self.repo = hg.repository(ui=con_ui, path=self.path)
        if user is None:
            self.repo.ui.setconfig('ui', 'interactive', False)
            self.user = self.repo.ui.username()
            self.repo.ui.setconfig('ui', 'interactive', True)
        else:
            self.user = self.main_win.str_to_external(user)
        self.repo.ui.quiet = True
        self.prompt_path = None

    def check_for_untracked(self, files):
        """
        This function get filelist and check if files have deleted, unknown or ignored status

        Keyword arguments:
            files -- list of file's names which are needed to commit (pathname relatively of repository's root)

        Return value:
            ([modified, added and removed], [deleted], [unknown], [ignored])
        """

        m = match.exact('.', '', [self.main_win.str_to_external(f, True) for f in files])
        # return status
        # format = ([modified], [added], [removed], [deleted], [unknown],
        #           [ignored], [clean])
        stat = self.repo.status('.', None, m, True, True, True)
        return ([self.main_win.str_to_internal(f, True) for f in stat[0] + stat[1] + stat[2]],
                [self.main_win.str_to_internal(f, True) for f in stat[3]],
                [self.main_win.str_to_internal(f, True) for f in stat[4]],
                [self.main_win.str_to_internal(f, True) for f in stat[5]])

    def do_commit(self, files, message):
        """
        This function do commit files to current repository.

        Keyword arguments:
            files -- list of file's names which are needed to commit (pathname relatively of repository's root)
            message -- text message of changeset

        Return value:
            true - if ok, false - if no changes

        Exceptions:
        """
        m = match.exact('.', '', [self.main_win.str_to_external(f, True) for f in files])
        return bool(self.repo.commit(self.main_win.str_to_external(message), self.user, match=m))

    def add(self, files):
        """
        This function add files to current repository.

        Keyword arguments:
            files -- list of file's names which are needed to add (pathname relatively of repository's root)
        """

        m = match.exact('.', '', [self.main_win.str_to_external(f, True) for f in files])
        if util.version() < '2.1':
            cmdutil.add(self.repo.ui, self.repo, m, False, None, '')
        else:
            cmdutil.add(self.repo.ui, self.repo, m, False, None, '', True)

    def remove_from_version_control(self, files):
        """
        This function remove files from current repository (don't remove files from filesystem).

        Keyword arguments:
            files -- list of file's names which are needed to remove (pathname relatively of repository's root)
        """

        wlock = self.repo.wlock()
        try:
            self.repo[None].forget([self.main_win.str_to_external(f, True) for f in files])
        finally:
            wlock.release()

    def __get_changeset_elements(self, chlist, repo=None):
        """
        This function return list elements of changesets

        Keyword arguments:
            chlist -- list of changesets
            repo -- other repository that is source of chlist
                    e.g. bundlerepo for incoming

        Return value:
            log[i][j] - 2-D list information of file's changesets

        Log format:
            log[i] = (revision, hash, branch, user, date, message, extra, files)
        """
        if repo is None:
            repo = self.repo
        log = []
        if chlist is None:
            return log
        for i in chlist:
            changeset = repo[i]
            log.append((changeset.rev(), changeset.hex(),
                        self.main_win.str_to_internal(changeset.branch()),
                        [self.main_win.str_to_internal(b) for b in changeset.bookmarks()],
                        self.main_win.str_to_internal(changeset.user()),
                        changeset.date(),
                        self.main_win.str_to_internal(changeset.description()),
                        changeset.extra(),
                        [self.main_win.str_to_internal(f, True)
                         for f in changeset.files()]))
        return log

    def get_log(self, files):
        """
        This function return log of local files

        Keyword arguments:
            files -- list of file's name (pathname relatively of repository's root)

        Return value:
            log[i][j] - 2-D list information of file's changesets

        Log format:
            log[i] = (revision, hash, branch, user, date, message, extra, files)
        """
        chlist = []
        files = set([self.main_win.str_to_external(f, True) for f in files])
        for i in self.repo:
            chset_files = set(self.repo[i].files())
            if len(files.intersection(chset_files)) > 0:
                chlist.append(i)
        return self.__get_changeset_elements(chlist)

    def get_full_log(self):
        """
        This function return log of repository


        Return value:
            log[i][j] - 2-D list information of changesets

        Log format:
            log[i] = (revision, hash, branch, user, date, message, extra, files)
        """
        return self.__get_changeset_elements(self.repo)

    def get_files(self):
        """
        This function return all files in local repository with statuses.

        Return value:
            [(file, status)] - list of files with statuses

        Statuses:
            M - modified
            A - added
            R - removed
            ! - deleted (but still under control)
            ? - unknown (not under control but not ignored)
            I - ignored
            C - clean
        """

        files = []
        # return status
        # format = ([modified], [added], [removed], [deleted], [unknown],
        #           [ignored], [clean])
        stat = self.repo.status('.', None, None, True, True, True)
        for f in stat[0]:
            files.append((self.main_win.str_to_internal(f, True), u'M'))
        for f in stat[1]:
            files.append((self.main_win.str_to_internal(f, True), u'A'))
        for f in stat[2]:
            files.append((self.main_win.str_to_internal(f, True), u'R'))
        for f in stat[3]:
            files.append((self.main_win.str_to_internal(f, True), u'!'))
        for f in stat[4]:
            files.append((self.main_win.str_to_internal(f, True), u'?'))
        for f in stat[5]:
            files.append((self.main_win.str_to_internal(f, True), u'I'))
        for f in stat[6]:
            files.append((self.main_win.str_to_internal(f, True), u'C'))
        return files

    def get_status(self, files):
        """
        This function return status of files

        Keyword arguments:
            files -- list of file's name (pathname relatively of repository's root)

        Return value:
            status[i] - list of status in order file's names

        Statuses:
            M - modified
            A - added
            R - removed
            ! - deleted (but still under control)
            ? - unknown (not under control but not ignored)
            I - ignored
            C - clean
        """
        status = []
        files = [self.main_win.str_to_external(f, True) for f in files]
        m = match.exact('.', '', files)
        # return status
        # format = ([modified], [added], [removed], [deleted], [unknown],
        #           [ignored], [clean])
        stat = self.repo.status('.', None, m, True, True, True)
        for f in files:
            if f in stat[0]:
                status.append(u'M')
            elif f in stat[1]:
                status.append(u'A')
            elif f in stat[2]:
                status.append(u'R')
            elif f in stat[3]:
                status.append(u'!')
            elif f in stat[4]:
                status.append(u'?')
            elif f in stat[5]:
                status.append(u'I')
            elif f in stat[6]:
                status.append(u'C')
            else:
                status.append(u' ')
        return status

    def __get_remote_path(self, push_path):
        """
        Get path for remote repo

        Keyword arguments:
            push_path -- is True find path for push otherwise - for pull

        Return value:
            path for remote repo or None if can't find any path
        """

        ui = self.repo.ui
        if push_path:
            path = ui.expandpath('default-push', 'default')
        else:
            path = ui.expandpath('default')
        if path in ('default-push', 'default'):
            if self.prompt_path is not None:
                path = self.prompt_path
            else:
                prompt_win = PromptWindow(_(u'Remote repo path:'))
                path = self.main_win.show_modal_window(prompt_win).get(u'value')
                self.prompt_path = path
        return path

    def __incoming(self, path=None):
        """
        This function return list of incoming revisions

        Keyword arguments:
            path -- path or URL of other repository

        Return value:
            (repo, diff) -- other repo and list of hashes of incoming
                                    revisions
        """

        if self.__updating:
            return (None, [])
        try:
            self.__updating = True
            # if no path specify use default path
            if path is None:
                path = self.__get_remote_path(False)
                if path is None:
                    self.server_error = NO_SERVER_ERROR_MESSAGE
                    return (None, [])
            # get repository object of bundle repository if it has same path,
            # otherwise make update with new path
            if (self.bundle_repo is not None and path != self.__success_bundle_path) or \
                (self.bundle_repo is None and path != self.__supposed_bundle_path):
                self.update_bundle_repo(path, force=True)
            # get changesets new for local repo from other repo
            repo = self.bundle_repo
            diff = self.bundle_diff
            return (repo, diff)
        finally:
            self.__updating = False

    def get_incoming(self, path=None):
        """
        This function return list of incoming revisions.
        Warning: this function doesn't get updates from remote server
        if bundle_repo already has instance of repo with same path.
        If you want to force update information about remote server state
        you should call update_bundle_repo(path) before.

        Keyword arguments:
            path -- path or URL of other repository

        Return value:
            log[i][j] - 2-D list information of file's changesets

        Log format:
            log[i] = (revision, hash, branch, user, date, message, extra, files)
        """

        # get changesets new for local repo from other repo
        repo, diff = self.__incoming(path)
        return self.__get_changeset_elements(diff, repo)

    def __outgoing(self, path=None):
        """
        This function return list of outgoing revisions

        Keyword arguments:
            path -- path or URL of other repository

        Return value:
            list of hashes of outgoing revisions
        """

        if self.__updating:
            return []
        try:
            self.__updating = True
            # if no path specify use default-push or default path
            if path is None:
                path = self.__get_remote_path(True)
                if path is None:
                    if self.server_error is None:
                        self.server_error = NO_SERVER_ERROR_MESSAGE
                    return []
            try:
                # get repository object of other repository (bundle if it exists and
                # have same path)
                if self.bundle_repo is not None and path == self.__success_bundle_path:
                    other_repo = self.bundle_repo
                elif self.bundle_repo is None and path == self.__supposed_bundle_path:
                    return None
                else:
                    other_repo = self.get_remote_repo(path)
                # get changesets common for other repo and local repo
                # return value of findcommonoutgoing() changes since version 2.1
                if util.version() < '2.1':
                    common, out_heads = discovery.findcommonoutgoing(self.repo, other_repo, commoninc=self.commoninc)
                    missing = self.repo.changelog.findmissing(common, out_heads)
                else:
                    og = discovery.findcommonoutgoing(self.repo, repo_or_peer(other_repo), commoninc=self.commoninc)
                    missing = og.missing
            except Exception:
                self.server_error = SERVER_ERROR_MESSAGE
                return None
            # get changesets new for other repo from local repo
            return missing
        finally:
            self.__updating = False

    def get_outgoing(self, path=None):
        """
        This function return list of outgoing revisions.
        Warning: this function doesn't get updates from remote server
        if bundle_repo already has instance of repo with same path.
        If you want to force update information about remote server state
        you should call update_bundle_repo(path) before.

        Keyword arguments:
            path -- path or URL of other repository

        Return value:
            log[i][j] - 2-D list information of file's changesets

        Log format:
            log[i] = (revision, hash, branch, user, date, message, extra, files)
        """

        # get changesets new for other repo from local repo
        diff = self.__outgoing(path)
        # None means error
        if diff is None:
            return None
        return self.__get_changeset_elements(diff)

    def __get_changed_files(self, path=None):
        """
        This function return list of files that presents in incoming revisions,
        presents in outgoing revisions and changed locally

        Keyword arguments:
            path -- path or URL of other repository

        Return value:
            (in, out, local) - 3 sets of files: from incoming revisions, from
                            outgoing revisions, changed locally
        """

        # get incoming revisions
        repo, in_diff = self.__incoming(path)
        if in_diff is None:
            return ([], [], [])
        # no incoming changes - no conflicts
        if len(in_diff) == 0:
            return ([], [], [])
        # get outgoing revisions
        out_diff = self.__outgoing(path)
        # get changed files in incoming revisions
        in_files = set([f for chset in in_diff for f in repo[chset].files()])
        # get changed files in outgoing revisions
        if out_diff is not None:
            out_files = set([f for chset in out_diff
                           for f in self.repo[chset].files()])
        else:
            out_files = None
        # get locally changed (modified, added, removed)
        # but not commited files
        changed_files = set([f for l in self.repo.status()[:3] for f in l])
        return (in_files, out_files, changed_files)

    def get_conflict_files(self, path=None):
        """
        This function return list of files that presents in incoming revisions
        and presents in outgoing revisions or changed locally

        Keyword arguments:
            path -- path or URL of other repository

        Return value:
            list of conflict files
        """

        in_files, out_files, changed_files = self.__get_changed_files(path)
        if out_files is None:
            return None
        # get union of locally changed and outgoing files
        out_files.update(changed_files)
        # get common files
        conflicts = in_files.intersection(out_files)
        return [self.main_win.str_to_internal(f, True) for f in conflicts]

    def get_merge_conflicts(self, path=None, file=None):
        """
        This function return list of files that can't be merged automatically
        or merge result if file specified

        Keyword arguments:
            path -- path or URL of other repository
            file -- name of file to merge

        Return value:
            list of conflict files or merge result
        """

        in_files, out_files, wc_files = self.__get_changed_files(path)
        # no input changes - no conflicts
        if len(in_files) == 0:
            return []

        if file is not None:
            file = self.main_win.str_to_external(file, True)

        common, incoming, rheads = discovery.findcommonincoming(self.repo, repo_or_peer(self.bundle_repo))
        actx = [self.repo[c] for c in common]
        if util.version() <= '2.8.2':
            incom = [self.bundle_repo[self.bundle_repo.branchtags()[a.branch()]] for a in actx]
        else:
            incom = [self.bundle_repo[self.bundle_repo.branchmap()[a.branch()][0]] for a in actx]
        out = [self.repo[a.branch()] for a in actx]
        wc = self.repo[None]
        workpath = self.workdir + '/'


        def try_merge(src_rev, file, i):
            """
            merge file from src_rev with incoming and return merge result

            Keyword arguments:
                src_rev -- wc or out
                file -- file to check
                i -- index of branch for actx, incom and out

            Return value:
                merge result
            """

            result = None
            # copy file and try to merge copy
            with open(workpath + 'merge.tmp', 'w') as out_file:
                out_file.write(src_rev.filectx(file).data())
            try:
                afctx = actx[i].filectx(file)
            except mercurial.error.LookupError:
                afctx = None
            r = filemerge.filemerge(self.repo, src_rev.p1().node(), file,
                wc.filectx('merge.tmp'), incom[i].filectx(file), afctx)
            # if merge failed then return merge result
            if r is not None and r > 0:
                with open(workpath + 'merge.tmp', 'r') as merge_result:
                    result = merge_result.read()
                os.unlink(workpath + 'merge.tmp.orig')
            os.unlink(workpath + 'merge.tmp')
            return result

        def check_files(src_rev, files, i):
            """
            Check filelist for merge conflicts (merge src_rev with incoming)

            Keyword arguments:
                src_rev -- wc or out
                files -- files to check
                i -- index of branch for actx, incom and out

            Return value:
                list of conflicted files
            """

            result = []
            for f in files:
                # copy file and try to merge copy
                with open(workpath + 'merge.tmp', 'w') as out_file:
                    out_file.write(src_rev.filectx(f).data())
                try:
                    afctx = actx[i].filectx(f)
                except mercurial.error.LookupError:
                    afctx = None
                try:
                    ifctx = incom[i].filectx(f)
                except mercurial.error.LookupError:
                    # if file deleted from incoming but changed locally
                    # there should be conflict
                    result.append(f)
                else:
                    r = filemerge.filemerge(self.repo, src_rev.p1().node(), f,
                        wc.filectx('merge.tmp'), ifctx, afctx)
                    os.unlink(workpath + 'merge.tmp')
                    # if merge failed then put this file to result
                    if r is not None and r > 0:
                        result.append(f)
                        os.unlink(workpath + 'merge.tmp.orig')
            return result

        try:
            # try to merge only using internal merge
            if os.environ.has_key('HGMERGE'):
                hgmerge = os.environ['HGMERGE']
            else:
                hgmerge = None
            os.environ['HGMERGE'] = 'internal:merge'
            # hide merge conflicts warning
            ui_ferr = self.repo.ui.ferr
            self.repo.ui.ferr = open(os.devnull, 'w')
            # get files that can provide conflicts
            result = []
            for i in range(0, len(actx)):
                # if work copy isn't child of last revision not see it files
                # because commit of it make new branch anyway
                if len(wc_files) == 0 or wc.p1() != out[i]:
                    # no local changes - no conflicts
                    if len(out_files) != 0:
                        if file is not None:
                            return try_merge(out[i], file, i)
                        else:
                            files = in_files.intersection(out_files)
                            result.extend(check_files(out[i], files, i))
                else:
                    # if there are no out revisions try to update workdir to input
                    if len(out_files) == 0:
                        if file is not None:
                            return try_merge(wc, file, i)
                        else:
                            files = in_files.intersection(wc_files)
                            result.extend(check_files(wc, files, i))

                    # if there are some out revisions try to "commit" workdir and
                    # merge in and out: at first try merge input with files from
                    # workdir, then try merge input with files from out revs (not
                    # changed in workdir)
                    else:
                        in_wc_files = in_files.intersection(wc_files)
                        in_out_files = in_files.intersection(out_files.difference(wc_files))
                        if file is not None:
                            if file in in_wc_files:
                                return try_merge(wc, file, i)
                            elif file in in_out_files:
                                return try_merge(out[i], file, i)
                        else:
                            result.extend(check_files(wc, in_wc_files, i))
                            result.extend(check_files(out[i], in_out_files, i))
            return [self.main_win.str_to_internal(f, True) for f in result]
        finally:
            if hgmerge is not None:
                os.environ['HGMERGE'] = hgmerge
            else:
                del os.environ['HGMERGE']
            self.repo.ui.ferr.close()
            self.repo.ui.ferr = ui_ferr

    def update_bundle_repo(self, path=None, force=False):
        """
        This function check other repository for changes.

        Keyword arguments:
            path -- path or URL of other repository
            force -- not use self.__updating
        """

        if self.__updating and not force:
            return
        try:
            if not force:
                self.__updating = True
            ui = self.repo.ui
            # if no path specify use default path
            if path is None:
                path = self.__get_remote_path(False)
                if path is None:
                    self.server_error = NO_SERVER_ERROR_MESSAGE
                    return
            self.__supposed_bundle_path = path
            try:
                # get repository object of other repository
                other_repo = self.get_remote_repo(path)
                clear_old_bundle = self.clear_bundle
                # get bundlerepo
                self.bundle_repo, self.bundle_diff, self.clear_bundle = \
                    bundlerepo.getremotechanges(ui, self.repo, repo_or_peer(other_repo))
                # If paths are the same then bundle repo include out revs
                # so there is no out revs from repo to bundle repo and
                # repo should be compared straight to remote repo
                if self.repo.path == self.bundle_repo.path:
                    self.commoninc = discovery.findcommonincoming(self.repo, repo_or_peer(other_repo))
                else:
                    self.commoninc = discovery.findcommonincoming(self.repo, repo_or_peer(self.bundle_repo))
            except Exception:
                self.server_error = SERVER_ERROR_MESSAGE
            else:
                # clear old bundle
                if clear_old_bundle is not None:
                    clear_old_bundle()
                self.__success_bundle_path = path
        finally:
            if not force:
                self.__updating = False

    def clear(self):
        """
        This function should to be called before close application.
        """

        if self.clear_bundle is not None:
            self.clear_bundle()

    def push(self, path=None, revs=None):
        """
        This function pushes local revisions to remote repository

        Keyword arguments:
            path - path to remote repository
            revs - revision to push
        """

        if path is None:
            path = self.__get_remote_path(True)
            if path is None:
                if self.server_error is None:
                    self.server_error = NO_SERVER_ERROR_MESSAGE
                return False
        try:
            remote_repo = self.get_remote_repo(path)
            if util.version() <= '3.1':
                self.repo.push(remote_repo, revs=revs)
            else:
                exchange.push(self.repo, remote_repo, revs=revs)
            if self.bundle_repo is None or self.repo.path == self.bundle_repo.path:
                self.commoninc = discovery.findcommonincoming(self.repo, repo_or_peer(remote_repo))
            else:
                self.commoninc = discovery.findcommonincoming(self.repo, repo_or_peer(self.bundle_repo))
        except Exception:
            self.server_error = SERVER_ERROR_MESSAGE
            return False
        else:
            return True

    def pull(self, path=None, revs=None):
        """
        This function pulls remote revisions to local repository

        Keyword arguments:
            path - path to remote repository
            revs - revisions to pull
        """

        if path is None:
            path = self.__get_remote_path(False)
            if path is None:
                self.server_error = NO_SERVER_ERROR_MESSAGE
                return
        try:
            other_repo = self.get_remote_repo(path)
            if util.version() <= '3.1':
                self.repo.pull(other_repo, heads=revs)
            else:
                exchange.pull(self.repo, other_repo, heads=revs)
            self.update_bundle_repo(path)
        except Exception:
            self.server_error = SERVER_ERROR_MESSAGE
            return False
        else:
            return True

    def revert(self, files, revision=None):
        """
        This function reverts selected files

        Keyword arguments:
            files - list of files
            revision - number of revision (if not specified, last revision is used)
        """

        try:
            ui = self.repo.ui
            commands.revert(ui, self.repo, *[self.main_win.str_to_external(f, True) for f in files], rev=revision)
        except util.Abort as e:
            self.local_error = unicode(e)
            return False
        else:
            return True

    def update(self, revision, main_window=None):
        try:
            ui = self.repo.ui
            ui.ferr = sys.stderr
            ui.fout = sys.stdout
            ui.fin = sys.stdin
            if main_window is not None:
                main_window.hide()
            commands.update(ui, self.repo, rev=revision)
        except util.Abort as e:
            self.local_error = unicode(e)
            if main_window is not None:
                main_window.unhide()
            return False
        else:
            if main_window is not None:
                main_window.unhide()
            return True
    def rollback(self, main_window=None, dry_run=False):
        """
        Делает rollback.
        Если main_window == None и dry_run == False,
        делает rollback не спрашивая.
        В противном случае сначала имитирует rollback,
        узнавая, что он откатит, затем спрашивает разрешения
        у пользователя, после чего выполняет rollback.'
        Если dry_run == True, то rollback только имитируется

        Аргументы:
            main_window - главное окно (для отображения модального окна)
            dry_run - имитировать rollback
        """

        if main_window is None:
            self.repo.rollback(dry_run)
        else:
            old_ui = self.repo.ui
            new_ui = ui.ui()
            mb = MessageBuffer(self.main_win)
            new_ui.fout = mb
            new_ui.ferr = mb
            self.repo.ui = new_ui

            if self.repo.rollback(dryrun=True):
                self.repo.ui = old_ui
                msg = ErrorWindow(mb.get_content())
                main_window.show_modal_window(msg)
            else:
                #Блокируем репозиторий до нажатия OK или Cancel
                lock = self.repo.lock()
                wlock = self.repo.wlock()
                self.repo.ui = old_ui
                rollback_todo = mb.get_content()
                msg = OKCancelWindow(_(u"The following will be performed "
                  u"during rollback:\n{0}\nAre you sure want to proceed?").
                  format(rollback_todo), _(u"Rollback"))
                if main_window.show_modal_window(msg)[u'button'] == u"OK":
                    release(lock,wlock)
                    self.repo.rollback(dryrun=dry_run)
                else:
                    release(lock,wlock)

    def tag(self, revision, tag, local):
        """
            Функция tag производит тегирование указанной функции без коммита

            Аргументы:
                revision - номер тегируемой ревизии (int)
                tag - тег (string)
                local - признак локальности тега (bool)

            Возвращаемое значение:
                True - в случае успеха
                False - в случае неудачи
        """
        try:
            commands.tag(self.repo.ui, self.repo, self.main_win.str_to_external(tag), local=local,
                            rev=revision, force=True)
        except util.Abort as e:
            self.tag_error = unicode(e)
            return False
        else:
            return True

    def diff(self, filename, rev1=None, rev2=None, changes=False, remote_rev1=False, remote_rev2=False):
        """
        This function return diff for file.
        If both rev1 and rev2 are None then compare workdir file to parent revision.
        If only rev1 is not None then compare workdir file to this revision.
        If both rev1 and rev2 specified then compare file between that revisions.

        If changes is True then show changes in rev1(diff between rev1 and its parent revision), rev2 ignored

        Keyword arguments:
            filename - name of file for compare
            rev1 - first revision for compare
            rev2 - second revision for compare
            changes - show changeset for rev1
            remote_rev1 - if True rev1 will be lookup in remote (bundle) repo
            remote_rev2 - if True rev2 will be lookup in remote (bundle) repo
        """

        br = self.bundle_repo
        old_repo_getitem = type(self.repo).__getitem__
        if '__getitem__' in dir(type(self.bundle_repo)):
            old_bundle_repo_getitem = type(self.bundle_repo).__getitem__
        else:
            old_bundle_repo_getitem = None

        def new_repo_getitem(self, idx):
            """
            Temporary replace of __getitem__ method of repo for find diff
            between revs in different repos (local and bundle)
            """

            try:
                return old_repo_getitem(self, idx)
            except Exception:
                return old_bundle_repo_getitem(br, idx)

        old_repo_status = type(self.repo).status

        def new_repo_status(self, *args, **kwargs):
            """
            Temporary replace of status method of repo for find diff
            between revs in different repos (local and bundle)
            """

            res = old_repo_status(self, *args, **kwargs)
            if len([l for sl in res for l in sl]) > 0:
                return res

            m = kwargs.get('match')
            if m is None:
                for a in args:
                    if isinstance(a, match.match):
                        m = a
                        break
            if m is not None:
                res[0].extend(m.files())
            return res

        filename = self.main_win.str_to_external(filename, True)

        if remote_rev1:
            repo1 = self.bundle_repo
        else:
            repo1 = self.repo
        if remote_rev2:
            repo2 = self.bundle_repo
        else:
            repo2 = self.repo

        try:
            # find repo context for specified revisions
            if changes:
                ctx1 = repo1[rev1]
                ctx2 = ctx1.p1()
            else:
                if rev2 is None:
                    ctx1 = self.repo[None]
                    ctx2 = rev1 is not None and repo1[rev1] or self.repo[None].p1()
                else:
                    ctx1 = repo1[rev1]
                    ctx2 = repo2[rev2]
                if remote_rev1 or remote_rev2:
                    type(self.repo).__getitem__ = new_repo_getitem
                    if '__getitem__' in dir(type(self.bundle_repo)):
                        type(self.bundle_repo).__getitem__ = new_repo_getitem
                    type(self.repo).status = new_repo_status

            # get content of file in specified revisions
            try:
                file2 = [[self.main_win.str_to_internal(s), False] for s in mdiff.splitnewlines(ctx1.filectx(filename).data())]
            except (mercurial.error.LookupError, IOError):
                file2 = []
            try:
                file1 = [[self.main_win.str_to_internal(s), False] for s in mdiff.splitnewlines(ctx2.filectx(filename).data())]
            except (mercurial.error.LookupError, IOError):
                file1 = []

            m = match.exact('.', '', [filename])
            _diffstrs = [d for d in ctx1.diff(ctx2=ctx2, match=m)]
            if len(_diffstrs) > 0:
                diffstrs = (self.main_win.str_to_internal(s) for s in mdiff.splitnewlines(_diffstrs[-1]))
            else:
                return (file1, file2)

            rem_idx = None
            add_idx = None
            for s in diffstrs:
                if s.startswith(u'@@'):
                    rem_idx = int(s.split(u' ')[1][1:].split(u',')[0]) - 1
                    add_idx = int(s.split(u' ')[2][1:].split(u',')[0]) - 1
                elif rem_idx is None or add_idx is None:
                    continue
                else:
                    action = s[0]
                    if action == u'-':
                        file1[rem_idx][1] = True
                        rem_idx += 1
                    elif action == u'+':
                        file2[add_idx][1] = True
                        add_idx += 1
                    elif action == u' ':
                        rem_idx += 1
                        add_idx += 1
            return (file1, file2)
        # restore changed repo functions
        finally:
            type(self.repo).__getitem__ = old_repo_getitem
            if '__getitem__' in dir(type(self.bundle_repo)):
                type(self.bundle_repo).__getitem__ = old_bundle_repo_getitem
            type(self.repo).status = old_repo_status

    def get_changes_for_revision(self, revision, repo=None):
        """
        This function return list of files changed in selected revision

        Keyword arguments:
            revision - revision for find filelist
            repo - repository for find changes (local or bundle)

        Return value:
            Return list of tuples (filename, status)
        """

        files = []
        if repo is None:
            repo = self.repo
        stat = repo.status(repo[revision].p1().node(), revision, None, None, True, True, True)
        for f in stat[0]:
            files.append((self.main_win.str_to_internal(f, True), u'M'))
        for f in stat[1]:
            files.append((self.main_win.str_to_internal(f, True), u'A'))
        for f in stat[2]:
            files.append((self.main_win.str_to_internal(f, True), u'R'))
        for f in stat[3]:
            files.append((self.main_win.str_to_internal(f, True), u'!'))
        for f in stat[4]:
            files.append((self.main_win.str_to_internal(f, True), u'?'))
        for f in stat[5]:
            files.append((self.main_win.str_to_internal(f, True), u'I'))
        for f in stat[6]:
            files.append((self.main_win.str_to_internal(f, True), u'C'))
        return files

    def get_parent(self, rev=None, filename=None, remote=False):
        """
        Return number of parent revision

        Keyword arguments:
            rev - revision to get parent
            filename - if is not None return parent for this file
                (revision number, where this file was last modified)
            remote - if True rev will be lookup in remote (bundle) repo

        Return value:
            Return number of parent (first if it has 2 parents) of rev
        """

        if remote:
            if self.bundle_repo is not None:
                 ctx = self.bundle_repo[rev]
            else:
                return -1
        else:
            ctx = self.repo[rev]
        if filename is None:
            parents =  ctx.parents()
        else:
            parents = ctx.filectx(self.main_win.str_to_external(filename, is_filename=True)).parents()
        if parents:
            return parents[0].rev()
        else:
            return -1

    def view(self, filename, rev=None, remote_rev=False):
        """
        This function return file content.
        If rev is None then return work copy file content.
        If rev is not None then return file content for this revision.

        If changes is True then show changes in rev1(diff between rev1 and its parent revision), rev2 ignored

        Keyword arguments:
            filename - name of file for show
            rev - revision for show file content
            remote_rev - if True rev will be lookup in remote (bundle) repo
        """

        if remote_rev:
            repo = self.bundle_repo
        else:
            repo = self.repo
        return [self.main_win.str_to_internal(s).expandtabs(4)
                for s in repo[rev].filectx(self.main_win.str_to_external(filename, is_filename=True))
                                  .data().splitlines()]
